package snafu.framework.worlds;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import snafu.framework.agents.Agent;
import snafu.framework.objects.WorldObject;
import snafu.framework.objects.WorldObjectItself;
import snafu.framework.states.State;
import snafu.framework.states.StateItself;
import snafu.framework.view.Access;
import snafu.implementation.states.literals.position.PositionLiteralItself;
import snafu.implementation.states.literals.surround.SurroundLiteral;
import snafu.utility.Cast;

public abstract class WorldMap {
	protected final int PERCEPTION_WIDTH = 1;
	protected int width, height;
	protected Tile[][] tiles;
	
	protected Map<UUID, WorldObject> mapping;
	
	
	public WorldMap(int width, int height) {
		this.width = width;
		this.height = height;

		tiles = new Tile[width][height];
		mapping = new HashMap<UUID, WorldObject>();
		
		for (int y = 0; y < height; y++)
			for (int x = 0; x < width; x++)
				tiles[x][y] = new Tile(new Position(x, y));
	}
	
	public int getWidth() {
		return width;
	}
	
	public int getHeight() {
		return height;
	}
	
	public void fillWith(Class<? extends WorldObjectItself<?>> classReference) {
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				try {
					// create new object and set position
					WorldObjectItself<?> object = Cast.cast(classReference.newInstance(), WorldObjectItself.class);
					StateItself state = Cast.cast(object.getState(), StateItself.class);
					state.setLiteral(new PositionLiteralItself(new Position(x, y), Access.PRIVATE));
					// add object to map
					addObject(new Position(x, y), object);
				} catch (InstantiationException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public Tile get(Position position) {
		if (position == null || position.x < 0 || position.x >= width || position.y < 0 || position.y >= height) {
			return null;
		}
		return tiles[position.x][position.y];
	}
	
	public WorldObject getObject(Position position, Class<?> clazz) {
		Tile tile = get(position);
		return tile.getObject(clazz);
	}
	
	public void set(Position position, Tile tile) {
		tiles[position.x][position.y] = tile;
	}
	
	public List<Tile> getTiles() {
		List<Tile> tileList = new ArrayList<Tile>();
		
		for (int y = 0; y < height; y++)
			for (int x = 0; x < height; x++)
				tileList.add(tiles[x][y]);
		
		return tileList;
	}
	
	/**
	 * Return the matching tile for a given mouse position
	 * @param x
	 * @param y
	 * @return
	 */
	public abstract Tile getTile(double x, double y);
	
	public abstract Point2D convertPositionToPoint(Position position);
	public abstract Point2D getDimension();
	public abstract Point2D getTileSize();
	public abstract WorldMapPanel getRepresentation();
	
	public void addObject(Position position, WorldObject object) {
		tiles[position.x][position.y].addObject(object);
		mapping.put(object.getId(), object);
		
		State state = object.getState();
		
		// add all surroundings to the map
		for (SurroundLiteral literal : state.getLiterals(SurroundLiteral.class)) {
			WorldObject surObject = literal.getValue();
			// add to position of the object
			get(position).addObject(surObject);
			// add to all surrounding tiles with a given range
			for (Tile tile : getPerceptionNeighborhood(literal.getRange(), getNeighborhood(position))) {
				tile.addObject(surObject);
			}
		}
	}
	
	public void removeObject(WorldObject object) {
		// can't remove null objects
		if (object == null) {
			return;
		}
		
		// get the current position of the object
		Position position = getPosition(object);
		// remove all replications of the object from the map
		while (position != null) {
			tiles[position.x][position.y].removeObject(object);
			mapping.remove(object);
			position = getPosition(object);
		}
	
		// get the current state of the object
		State state = object.getState();
		
		// remove all surroundings
		for (SurroundLiteral literal : state.getLiterals(SurroundLiteral.class)) {
			removeObject(literal.getValue());
		}
	}
	
	public Position getPosition(WorldObject object) {
		// TODO improve bad complexity
		for (int y = 0; y < height; y++)
			for (int x = 0; x < width; x++)
				if (tiles[x][y] != null && tiles[x][y].containsObject(object))
					return new Position(x, y);
		
		// object not found
		return null;
	}
	
	
	/**
	 * Returns a set of tiles that are in direct neighborhood of the tile on the given position.
	 * 
	 * @param from
	 *            the position of the tile
	 * @return a set of neighboring tiles, empty if none
	 */
	public abstract Set<Tile> getNeighborhood(Position from);
	
	public Set<Tile> getPerceptionNeighborhood(int n, Set<Tile> perceptionNeighborhood) {
		if (n <= 0) {
			return new HashSet<Tile>();
		}
		
		if (n == 1) {
			return perceptionNeighborhood;
		}
		System.out.println("" + n);
		
		Set<Tile> newPerceptionNeighborhood = new HashSet<Tile>(perceptionNeighborhood);
		for (Tile tile : perceptionNeighborhood) {
			newPerceptionNeighborhood.addAll(getNeighborhood(tile.getPosition()));
		}
		
		return getPerceptionNeighborhood(n - 1, newPerceptionNeighborhood);
	}
	

	/**
	 * Returns the tile that is directly in front of the tile on the given position with the given
	 * direction.
	 * 
	 * @param from
	 *            the position of the tile
	 * @param direction
	 *            the direction to face respective to the tile
	 * @return the tile in front, EmtpyTile if none
	 */
	public abstract Tile getFrontTile(Position from, Integer direction);
	
	/**
	 * Returns the next direction rotated to the left given a specific direction.
	 * 
	 * @param direction
	 *            the current direction
	 * @return the next left direction
	 */
	public abstract Integer getNextLeftDirection(Integer direction);
	
	/**
	 * Returns the next direction rotated to the right given a specific direction.
	 * 
	 * @param direction
	 *            the current direction
	 * @return the next right direction
	 */
	public abstract Integer getNextRightDirection(Integer direction);
	
	/**
	 * Creates a perception for a world object.
	 * 
	 * @param object
	 *            the object that needs the perception
	 * @return a world map containing all tiles and object necessary for the perception, EmptyWorld
	 *         if none
	 */
	public abstract WorldMap getPerceptionFor(Agent object);

	protected Tile createTileViewFor(Tile tile, Agent agent, Position position) {
		Tile newTile = new Tile(position);
		
		// add all object views from the old to the new tile
		for (WorldObject tileObject : tile.getWorldObjects()) {
			if (agent.canPercept(tileObject)) {
				System.out.println(agent + " percepted " + tileObject);
				// create the view for objects
				WorldObject objectView = (WorldObject) ((WorldObjectItself<?>) tileObject).createView(Access.PUBLIC);
				newTile.addObject(objectView);
			}
		}
		
		return newTile;
	}
	
	
	public WorldObject getObject(UUID id) {
		if (id == null) {
			return null;
		}
		WorldObject object = mapping.get(id);
		return object;
	}
	
	public Collection<WorldObject> getObjects() {
		return mapping.values();
	}
	
	/**
	 * Creates a string representation of the map. Will be called by the {@link #toString()} method.
	 * 
	 * @return the map as a string
	 */
	public abstract String output();
	
	@Override
	public String toString() {
		return output();
	}
}